Skip to content

fix(triggers): point repository.directory at the directory the trigger packages actually live in - #15993

Open
os-warren wants to merge 6 commits into
mainfrom
claude/issue-15478-stale-repository-directory
Open

fix(triggers): point repository.directory at the directory the trigger packages actually live in#15993
os-warren wants to merge 6 commits into
mainfrom
claude/issue-15478-stale-repository-directory

Conversation

@os-warren

@os-warren os-warren commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15478

@objectstack/trigger-record-change and @objectstack/trigger-schedule each declared a repository.directory under packages/plugins/ that does not exist in this repository, so the Repository deep link on their npm pages resolves to a 404 instead of to their source. Both are published today at 17.3.0.

package was now
@objectstack/trigger-record-change packages/plugins/plugin-trigger-record-change packages/triggers/trigger-record-change
@objectstack/trigger-schedule packages/plugins/plugin-trigger-schedule packages/triggers/trigger-schedule

The whole class, not just the two named

The card names two packages. The census was scripted rather than eyeballed, over git ls-files for every tracked package.json (81 files, node_modules excluded), comparing repository.directory both against the filesystem and against the manifest's own directory — a plausible-but-wrong path fails exactly as silently as the current one, so resolving is not sufficient on its own.

The script refuses to print any number unless its control fires in both directions: a directory known to exist must report EXISTS, and a deliberately impossible one must report MISSING.

CONTROL exists-case  packages/triggers/trigger-record-change      -> EXISTS  (want EXISTS)
CONTROL missing-case packages/__deliberately_impossible_15478__    -> MISSING (want MISSING)
CONTROL OK
before after
tracked package.json 81 81
declare repository.directory 57 57
resolves and equals own directory 55 57
mismatched 2 0
declares nothing 24 24

The two the card names were the entire mismatched set; the sweep found no third. Both were also the entire non-resolving set, so no manifest points at a directory that exists but belongs to someone else.

How the field went stale — corrected

⚠️ An earlier revision of this PR and its changeset said the field survived "two pure renames" and that "nothing rewrote the field". That was false, and the true account argues more strongly for a mechanical check. Measured commit by commit:

commit when what it actually did
f15d6f6f6 2026-06-12 07:43 A copy, not a rename — 26 files, +2222/-19, with all four trigger directories briefly coexisting. Each copied manifest differs from its original in exactly one line: name. So this commit edited the manifest and left directory pointing at the path it was copied from.
290c62514 2026-06-12 07:48 Deleted the original directories five minutes later (merged as f8f7c549c, #1751).
ea4941ad8 2026-06-12 08:09 The only pure rename: packages/plugins/trigger-* to packages/triggers/trigger-*, 16 files, 0 insertions, 0 deletions. This is where the second segment went wrong.
9a43e042f 2026-07-21 #3380 rewrote repository.url and bugs in both of these manifests. The stale directory line sits as unchanged context one line below the edited url, inside the same repository object, in a reviewed hunk.

⭐ That last row is why the correction was worth making. "Nothing rewrote the field" reads as the field was never in anyone's way. The opposite is true in the way that matters: one commit edited its immediate neighbour inside the same JSON object, and six weeks later an author had the dead line on screen and shipped past it. A rule is more clearly needed when a human looked at the line and still missed it — so this strengthens the case on #15991 rather than softening it.

Same-migration rot in a different key — one more, corrected here

docs/adr/0041-flow-trigger-family.md carries the same dead path in its ## References section as a source pointer:

- Shipped triggers: `packages/plugins/plugin-trigger-record-change`,
  `packages/plugins/plugin-trigger-schedule`

That is the same defect class from the same rename — a source deep link that now 404s — so it is corrected with the manifests and named here rather than left as unreported drift.

Deliberately left as written: the same record's rename table and narrative at lines 46, 48 and 97-100. Those name the old identifiers as the decision's own history, and the table's right-hand column is a package name, not a path — not a pointer by construction. The anchored replacement could not have touched them in any case (they carry no packages/plugins/ prefix), but the distinction is the point: a References pointer is live, a decision record's account of what it renamed is not.

The bounding scan is a repo-wide git grep, so this is a measured set and not a sample. Across all tracked files at the merge base, packages/plugins/plugin-trigger appeared in exactly 4 places — the 2 manifests and these 2 ADR lines — all 4 corrected. Remaining plugin-trigger matches are the ADR history above plus packages/cli/CHANGELOG.md, a published-version record that must not be rewritten.

⚠️ Stated precisely, because the population must include the file doing the counting: the intermediate spelling packages/plugins/trigger- occurs 0 times at the merge base and 1 time at head — that one occurrence is this PR's own changeset, quoting the history above. It is a quotation, not a pointer.

Other path-shaped manifest keys were swept in the same pass: exactly one homepage/bugs/repository.url value repo-wide embeds a repo-relative path (packages/console, which exists and is unrelated to this migration), and pnpm-workspace.yaml already lists packages/triggers/*, so no workspace glob went stale.

⭐ Why no gate caught it: the rule never existed

Not the wrong population, and not a missing clause in an existing rule set. No script in this repo reads repository.directory at all.

Stated as the measurement rather than a characterisation: across scripts/, 245 lines contain the token repository, and a grep for any read of a manifest repository.directory returns zero. The only code-shaped uses of the token are CI's github.repository context (process.env.GITHUB_REPOSITORY and the repository: keys building an owner/repo API context in release-github-releases.mjs). The bulk of the remaining hits are incidental substring matches in file paths and identifiers — the largest single concentration, 27 lines in scripts/engine-double-contract.pinned.json, is the filename sys-metadata-repository-…test.ts — plus prose in comments. (An earlier revision of this body called them all "CI context or comment prose", which was imprecise; the load-bearing claim, zero reads, is unchanged and was independently confirmed at 0 reads across 6061 tracked code/config files.)

The near-miss worth naming is scripts/check-published-files.mjs. It has exactly the right population — every non-private workspace package — and already parses each manifest and validates files, types, module and exports subpaths. All six of its invariants concern the files whitelist: what the tarball contains. The word repository occurs 0 times in it. Nothing asks where the tarball says its source lives. The notch is a category, not an off-by-one: this repo gates published package contents and does not gate published package provenance metadata.

Two adjacent gates confirm the notch rather than closing it: check-published-readme-links.mjs resolves links in published markdown (READMEs, not manifests), and check-adr-links.mjs matches only [text](target) markdown links — the stale ADR pointer above is an inline code span, so that gate cannot see it by form.

No gate is changed in this PR. The check is filed as #15991, split out on the instruction in this card's own triage comment ("if the gate turns out to want a decision … land the two string fixes first and split the gate out"). The decision is real and measured: 24 manifests declare nothing, 14 of them publishable (including @objectstack/trigger-api, a sibling in this very family), so whether declaring the field is mandatory is a policy call, not a correction.

Changeset, not skip-changeset

skip-changeset does not apply. Its rule is "publishes nothing from any package", and this changes published package metadata: repository.directory ships inside the npm tarball regardless of the files whitelist, so a fix that never publishes leaves both package pages 404ing. A patch changeset covers both packages. No skip-changeset label is set on this PR.

Verification

All runs below are on head 6c2957384. origin/main moved during review; dispatch-gates.mjs reported STALE TREE against the older head, so origin/main (f7db8f4fd) was merged — not rebased, and nothing force-pushed. The merge touched none of this PR's four files, brought in no merge=os-regen path, and the re-derivation after it is clean. This PR's diff against origin/main remains exactly those four files.

Gate family derived mechanically from the actual changed files — node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, which reads the change set from the merge base itself (4 paths), never hand-built. Every exit code captured immediately after a single redirected command, never through a pipe, and every command given its own cd.

All 20 path-derived gates EXIT=0: adr-anchors, changeset-gate-self-tests, cross-package-test-inputs, doc-authoring, doc-formula-expressions, dts-closure, dual-build-cjs-loads, llms-txt, merge-driver, objectui-changeset, org-identifier, override-consistency, page-declaration-shape, pm-governed-merges, published-files, slot-lookup, test-source-alias, turbo-task-graph, type-source-resolution, workspace-manifest-cycles — plus whole-tree nul-bytes EXIT=0.

check:doc-formula-expressions first answered 3 = PREREQUISITE NOT MET on the fresh worktree, which is not a pass and is not reported as one; after turbo run build (73/73 tasks) it re-ran EXIT=0. On the same built tree, check:dts-closure EXIT=0 (164/164 declaration files across 72 built packages) and check:dual-build-cjs-loads EXIT=0 (103 published require entry points across 66 packages load; 619 emitted CommonJS files parse).

The check named in dispatch, with its own control: node scripts/check-adr-0087-registration.mjs --self-test EXIT=0 (325 assertions over real temp git repos), then --base origin/main --head 6c2957384328dbc25e32ed044ab441fba0682ff4 EXIT=0 ("adds no declared-breaking changeset, 1 non-breaking changeset seen").

Carried forward from the pre-merge head e0fcc1455, where the diff was identical apart from the changeset prose corrected above: trigger-record-change 10 files / 101 tests passed, trigger-schedule 4 files / 57 tests passed, typecheck EXIT=0 for both with the script names echoed in the output (a --filter matching zero scripts exits 0 having run nothing, so the echo is what distinguishes a pass from a no-op). This revision changes only changeset prose, so the suites were not re-run and that is stated rather than implied.

⚠️ Wall-clock figures from these runs are shared-box readings taken under scripts/pm/os-verify-lock.sh, which excludes other locked runs and not unlocked sibling work.

The census script lives outside the repository tree; no scratch file was on disk during any whole-tree gate run, and the working tree is clean at 6c2957384.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

…r packages actually live in

`@objectstack/trigger-record-change` and `@objectstack/trigger-schedule` both
declared a `repository.directory` under `packages/plugins/` that does not exist
in this repository, so the "Repository" deep link on their npm pages resolves to
a 404 instead of to their source. Both are published at 17.3.0.

    @objectstack/trigger-record-change
      was packages/plugins/plugin-trigger-record-change
      now packages/triggers/trigger-record-change
    @objectstack/trigger-schedule
      was packages/plugins/plugin-trigger-schedule
      now packages/triggers/trigger-schedule

Residue of two same-day renames, not one: `plugin-trigger-*` -> `trigger-*`
inside `packages/plugins/` broke the first segment, and the later promotion of
`packages/plugins/trigger-*` to a first-class `packages/triggers/` directory
broke the second. Both were pure directory renames with zero content changes, so
nothing rewrote the field.

Scope as a measured set, not a general claim: across all 81 tracked package.json
files, 57 declare `repository.directory`; 55 resolved to the manifest's own
directory before this change and 57 of 57 resolve after it. The 24 manifests
that declare no `repository.directory` at all are untouched here.

ADR-0041's `## References` section carried the same dead path as a source
pointer and is corrected with it. The rename table earlier in that record
(`plugin-trigger-record-change` -> `@objectstack/trigger-record-change`) is the
decision's own history and is deliberately left as written.

The corrected value ships inside the npm tarball, so it reaches readers only by
being published: a patch changeset accompanies the fix rather than
`skip-changeset`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
@github-actions github-actions Bot added size/s dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tooling labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 2 changed file(s) yielded no anchor (packages/triggers/trigger-record-change/package.json, packages/triggers/trigger-schedule/package.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 2 changed package(s)).

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/triggers/trigger-record-change/package.json, packages/triggers/trigger-schedule/package.json) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a87163c89b96b57fa4d1002b63fa9e77d039d01bpackageMentionDocs.

Copy link
Copy Markdown
Collaborator Author

Clause-② contract review · PR #15993 (card #15478) · domain:services

Tier. CONTRACT_REVIEW_TIER = 'claude-fable-5-1' (scripts/pm/dispatch-gates.mjs:9852). This review ran under an explicit model: fable override attested by the PM seat; tier stated as override + self-report: claude-fable-5-1. No exact-match claim is made.

Independence. The dev was a separate os-dev subagent of the PM session and the dev line is a branch; this is not the self-review case.

Instrument. Own detached worktree /home/user/objectstack-review-15993 at e0fcc1455, merge-base aa6ba0623 re-derived. git status was empty before and after every run; no tracked file was mutated, and none was owed — a 4-line metadata fix with no gate change carries no pin obligation, and I agree with the dev that none is claimed.

Verdict: PASS

The fix, the class census, the sweep, the ADR boundary, the gate characterisation, the changeset decision and the vacuity guard all hold under my own instruments. One sentence of shipped prose is false (§2 below) and wants a one-line correction before undraft. It concerns why the field went stale, not the fix, and the true story strengthens #15991 rather than weakening it — but it ships as CHANGELOG text, so it should not go out as written.

1. Is the class closed? — Yes, re-derived with a stronger property

Own instrument (census-15993-review.mjs, out of tree), population: every tracked file with basename package.json at the ref (git ls-tree -r). It measures the dev's stated property — declared value equals dirname(manifest) — not merely existence, and records existence as a secondary column. It refuses to print unless four controls fire: a known directory reports EXISTS, packages/__deliberately_impossible_15993_review__ reports MISSING, the comparator flags a/c vs a/b/package.json as MISMATCH, and accepts a/b/ (trailing slash) as EQUAL. All four fired on both runs.

ref tracked files package.json declare EXISTS EQUALS dirname mismatch undeclared
merge-base aa6ba0623 7711 81 57 55/57 55/57 2 (the two named, existsAtRef=false) 24
head e0fcc1455 7712 81 57 57/57 57/57 0 24

The merge-base run is the firing control: it names exactly the two card manifests and nothing else. EXISTS and EQUALS agree at both refs, so no manifest points at a directory that exists but is someone else's.

2. Is the sweep complete? — Yes. The causal account is not.

Own git grep -F at both refs, population: all tracked files (7711 / 7712), six spellings:

spelling merge-base head
packages/plugins/plugin-trigger 4 occurrences / 3 files (ADR :194-195, 2 manifests) 1 file — the changeset, quoting the old value
packages/plugins/trigger- (intermediate) 0 1 — the changeset's own prose (line 17), quoting history
packages/triggers/plugin-trigger (hybrid) 0 0
bare plugin-trigger-record-change / -schedule / @objectstack/plugin-trigger ADR history + packages/cli/CHANGELOG.md npm-name release rows same

No third path spelling exists between the two states; the sweep has no hole. (Nit: "intermediate spelling occurs 0 times" is 1 at head — in the PR's own changeset, as a quotation. Not a pointer; noting it for precision only.)

But the two-rename story is wrong on the facts, and the dev's proof of it (ea4941ad8 is 16 files 0/0) covers only the second step. Measured:

  • 998c4e4ea (parent): packages/plugins/plugin-trigger-* exists, directory correct for that path.
  • f15d6f6f6 2026-06-12 07:43 — "feat(approvals): ADR-0042 SLA auto-escalation + ADR-0041 mechanical landing": 26 files, +2222/−19, 0 deletions. It copies the two packages to packages/plugins/trigger-* and leaves the old dirs in place (git ls-tree -d f15d6f6f6 packages/plugins/ lists all four). The copied manifests are byte-identical to the originals except line 2, name, which was edited — so this commit touched the manifest and left directory stale. Not a rename, not content-free.
  • f8f7c549c 2026-06-12 07:49 — fix: remove stale plugin-trigger-* directories left behind by the rename #1751 fix-remove-stale-plugin-trigger-dirs deletes the old dirs (a third same-day commit the account omits).
  • ea4941ad8 2026-06-12 08:09 — pure rename packages/plugins/trigger-*packages/triggers/trigger-*, 16 files, 0/0. ✓ as stated.
  • 9a43e042f 2026-07-21 (docs: update repo references for framework → objectstack rename #3380) "update repo references for framework → objectstack rename": edits repository.url and bugs in both these manifests; the stale "directory" line is unchanged context, one line below the edited url, in a reviewed hunk.

So the sentence "Both were pure directory renames that changed no file contents, so nothing rewrote the field" is false under any reading, and #15991's "no content-touching diff existed for a reviewer to notice" is false twice over — one edit changed the manifest's name, another put the stale line on screen and still missed it. That is the stronger argument for a mechanical gate (a human-reviewed hunk containing the line did not catch it), so the correction costs #15991 nothing.

Required before undraft — in .changeset/trigger-packages-repository-directory-15478.md, replace the last sentence of the "residue" paragraph with something true, e.g.: "The first step (f15d6f6f6) copied the packages and edited only their name; the second (ea4941ad8) was a pure 16-file rename; a later repo-wide url rewrite (9a43e042f, #3380) edited the same repository block and left directory as it was. Nothing checked the field, because nothing reads it." The same correction belongs on #15991 as a comment (the PR body carries the claim too, but the body does not ship).

3. ADR-0041 boundary — correct

## References was a live source pointer with the dead path and is corrected. Lines 46/48/97-100 are the decision's "today → becomes" record, and the "becomes" column is a package name, not a path — it is not a pointer by construction. Nothing reads the table as data: git grep 0041 -- scripts/ .github/ → 0. scripts/check-adr-links.mjs:237 matches only [text](target) markdown links, so the code-span pointer was invisible to it by form, exactly as the dev says.

4. Gate answer — verified, and the sentence #15991 works from is right

  • Population scripts/: 336 tracked files. 245 lines contain repository; the only code-shaped use is release-github-releases.mjs:588 (process.env.GITHUB_REPOSITORY) plus four repository: keys building its owner/repo API context. Zero reads of a manifest repository field. Precision nit: most of the remaining hits are invariant strings in scripts/adr-anchors/*.json, not only "comment prose" — the substance holds.
  • Repo-wide, population 6061 tracked .mjs/.js/.cjs/.ts/.tsx/.yml/.yaml/.sh files: repository.directory / repository["directory"] read 0 times. "The rule never existed" is true on a named set.
  • scripts/check-published-files.mjs: six invariants DECLARED / COMPLETE / SUFFICIENT / MINIMAL / REGISTERED / GATED, all over the files whitelist and exports; the word repository occurs 0 times in the file. Contents gated, provenance metadata not gated is an accurate characterisation. No gate changed in this diff (4 files, none under scripts/). ✓

5. Changeset decision — proved on the artifact

Real npm pack on both packages at head (no dist/ built): 3-entry tarballs, and package/package.json inside each carries repository.directory. The live registry (npm view … repository) shows both at 17.3.0 with the dead packages/plugins/plugin-trigger-* path today. Both are private: false, in .changeset/config.json's fixed group, ignore: []; PR labels are documentation, dependencies, size/s, tooling — no skip-changeset. A patch changeset is the only route by which this fix reaches the 404. ✓

6. Vacuity trap — confirmed in both directions

pnpm --filter '@objectstack/__no_such_pkg_15993_review__' typecheckexit 0, output "No projects matched the filters", no script line. The real filter echoes trigger-record-change typecheck$ tsc --noEmit && tsc --noEmit -p tsconfig.test.json and trigger-schedule typecheck$ tsc --noEmit. My first run was exit 2 on TS2307 (unbuilt @objectstack/core / @objectstack/spec/*) — a prerequisite failure, not reported as a pass; after building the 21-package dependency closure (exit 0), typecheckexit 0, both names echoed, 0 error TS. Each exit captured after a single redirected command.

7. Declared NOT MEASURED — sanity-checked

8. Prose guard

Checked: "entire mismatched set" (named set: 81/57, §1), "0 elsewhere" (named set: 7712 files, §2), "never existed" (named sets: 336 + 6061 files, §4), "pnpm-workspace already lists packages/triggers/*" (pnpm-workspace.yaml:7 ✓), "exactly one path-shaped homepage/bugs/repository.url" (packages/console ✓, over all 81). The one claim that fails is the temporally-phrased one in §2 — the species the brief warned about.


🤖 Generated with Claude Code

https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y


Generated by Claude Code

…and a later commit edited the adjacent `url`

The shipped changeset claimed the stale `repository.directory` survived two
pure renames and that "nothing rewrote the field". Measured, that is false in
three ways, and the true account is a stronger argument for a mechanical check
rather than a weaker one.

  f15d6f6  NOT a rename: a 26-file copy, +2222/-19, with all four
             trigger directories briefly coexisting. Each copied manifest
             differs from its original in exactly one line -- `name` -- so
             this commit edited the manifest and left `directory` stale.
  290c625  deleted the originals five minutes later.
  ea4941a  the only pure rename: 16 files, 0 insertions, 0 deletions.
  9a43e04  six weeks later, rewrote `repository.url` and `bugs` in both
             of these manifests. The stale `directory` line sits as
             unchanged context ONE LINE BELOW the edited `url`.

The last point is the one worth the correction. "Nothing rewrote the field"
reads as the field never having been in anyone's way; in fact an author edited
its immediate neighbour inside the same JSON object and the dead line was on
screen in a reviewed hunk. A rule is more clearly needed when a human looked at
the line and still missed it, so this strengthens the case on the gate card.

No code change: the four-line metadata fix is unchanged and its verification
still stands. This commit corrects prose only, in the file that ships to
release notes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
@os-warren
os-warren marked this pull request as ready for review September 5, 2026 18:23
@os-warren
os-warren requested a review from hotlong as a code owner September 5, 2026 18:23

Copy link
Copy Markdown
Collaborator Author

PM verification — undrafted and armed

Verified at head 6c2957384328dbc25e32ed044ab441fba0682ff4. The four-line fix is byte-identical to the reviewed head (both manifests' blobs unchanged), with the changeset as the firing control in the same comparison. The false sentence is gone (0 occurrences of "nothing rewrote the field"), 9a43e042f is named, and check-adr-0087-registration exit 0.

⭐ The correction is better than the correction I asked for

I asked for three facts. The rewritten paragraph found a fourth commit and reframes the whole account as a three-commit sequence on 2026-06-12: f15d6f6f6 copied the packages (26 files, +2222/−19, all four directories briefly coexisting) and edited exactly one line of each copied manifest — its name — leaving directory pointing at the path it was copied from; 290c62514 deleted the originals five minutes later; and only ea4941ad8 was the pure 16-file rename, which broke a second segment. Then six weeks later 9a43e042f rewrote repository.url and bugs in these same manifests, with the stale directory line as unchanged context one line below the edited url.

And it ends where the card should:

So the field was not merely never in anyone's way: one commit edited its immediate neighbour inside the same object, and a later reviewed hunk had the wrong line on screen. Nothing caught it because nothing reads it.

⇒ That last sentence is the gate finding stated in eight words, and it is now in the artifact that ships.

⭐ The scope paragraph is also stated as a measured set rather than a general claim — 81 tracked manifests, 57 declaring the field, 55 → 57 of 57 resolving, 24 undeclared and deliberately left to a policy question. That is the shape I have been asking for all session and it arrived without being asked for here.

⭐ And it declined a correction it could not reproduce

The reviewer's second nit attributed most repository hits under scripts/ to scripts/adr-anchors/*.json invariant strings. The dev measured 1 of 245 lines there; the real concentration is 27 lines in scripts/engine-double-contract.pinned.json, and they are incidental filename substrings (sys-metadata-repository-…test.ts). ⇒ It replaced the sentence with the measurement itself rather than "swapping one imprecise characterisation for another."

That is the right response to a reviewer being partly wrong: the reviewer's substance was right (the dev's "comment prose" was inaccurate), its specifics were not, and adopting the specifics on authority would have shipped a second false sentence in the act of fixing the first. ⛔ The load-bearing claim — 0 reads of repository.directory across 6061 code/config files — is untouched and independently re-confirmed.

It verified the reviewer before accepting it

All three findings re-derived with its own instruments: git ls-tree -d showing all four trigger directories coexisting (proving copy, not rename); the copied-vs-original manifest diff being exactly one line, "name"; and the 9a43e042f hunk showing the stale directory as unchanged context one line below the changed url.

One tooling note worth keeping

dispatch-gates.mjs flagged STALE TREE at the correction commit — naming scripts/engine-double-contract.pinned.json as a changed derivation input — and said it "still exited 0 — a well-formed answer about a tree nobody is on." ⭐ A tool that returns a valid answer and tells you it is about the wrong tree is doing something most gates do not: separating "this computation succeeded" from "this computation is about your situation." The merge was needed, not optional, and the tool is why that was knowable.

Nit 1 fixed as asked: stated as 0 at merge base, 1 at head, that one being the changeset's own quotation.

Gates on the merged head: 20 path-derived + whole-tree nul-bytes exit 0; check:doc-formula-expressions first exit 3 = PREREQUISITE NOT MET on a fresh worktree, built, re-run exit 0 — ⛔ not reported as a pass while it was a 3; check:dts-closure 164/164 across 72 packages; check:dual-build-cjs-loads 103 require entry points across 66 packages.

The correction is also posted on #15991, so that card does not carry the false account into whoever takes the gate work — and it strengthens the case there: a rule that only fires on directory moves would have missed 9a43e042f entirely, since that commit moved nothing.

Undrafted and auto-merge armed.


Generated by Claude Code

@os-warren
os-warren enabled auto-merge September 5, 2026 18:23

Copy link
Copy Markdown
Collaborator Author

CI red — measured NOT this PR's

Lint & Repo Gates is the only failing job on head 6c2957384 (run 33983205576, job 101352067958). Every other job in that run is green: Type Check · workspace, · source gates, · consumer gates, · debt ledger, and the TypeScript Type Check rollup.

The failing step, named: step 141 Merge-driver wiring gatepnpm check:merge-drivernode scripts/check-regen-pending.mjs --self-test✗ self-test failed -- 1 failure(s) (cases and floor).

The control that settles ownership. The identical step fails on main itself at f7db8f4fd — run 33981169123, job 101346594800, step 141, same name, same closing line. A check that is red on the base branch is not this PR's, and this PR touches neither scripts/check-regen-pending.mjs nor scripts/git-merge-regen.mjs nor .gitattributes (its whole diff is two package.json repository.directory values, one ADR reference line and a changeset).

Where it is owned. Already filed by other seats, ahead of this comment: #15992 (p0, domain:devx, pm:dispatched), plus #15990 and #15994 on the same failure. #15992 records the cause as the self-test's gate stubs shelling out to an unpinned pnpm -s, which Corepack now resolves to pnpm latest = 12.3.4, a version that rejects -s. ⚠️ That diagnosis is quoted from #15992, not measured here — what is measured here is only that the step fails identically on main.

Why no fix is ported into this PR. The rule I work under says to port an existing fix rather than wait. It does not apply here, and the repo's own gate is why: scripts/check-regen-pending.mjs is a single-writer path, and No other open PR may claim the same single-writer path is a required check on this PR — carrying that file here would red the very gate that is currently green, and collide with #15992's fix PR. So this PR waits for #15992 to land on main and then takes it by merging main.

Why no re-run. An unpinned launcher resolving to a version that rejects a flag is deterministic, not a flake. Re-running would reproduce it. The one re-run this PR is entitled to is left unspent.

Local, for what it is worth as context rather than as the argument: node scripts/check-regen-pending.mjs --self-test passes here at origin/main f7db8f4fd (git 2.43.0, exit 0, 0 ), so the failure is environment-bound to the CI launcher — consistent with #15992's account, and the reason it cannot be reproduced by reading the diff.

State of this PR: contract review returned and its corrections are in (06409ff97). Nothing here is waiting on the author. It is waiting on main going green.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Base branch recovered — taken, as the earlier comment said it would be.

main is green again at f50c394da — run 33986517218, conclusion success. The Merge-driver wiring gate is fixed at its source by #16002 (for #15992): the self-test's throwaway fixture carried no packageManager, so Corepack re-resolved the registry's latest dist-tag every run, and when latest moved to a pnpm major whose CLI rejects -s, the stub exited 2 without running. ⭐ That is why the same self-test exited 0 locally while red in CI — a different pnpm answered the fixture, so the local reading was never the control.

Merged origin/main into this branch and pushed: 6c295738434b232a73. No rebase, no force-push.

Verified the merge did not disturb what was reviewed — all four of this PR's files byte-identical, with a firing control:

.changeset/trigger-packages-repository-directory-15478.md      SAME
docs/adr/0041-flow-trigger-family.md                           SAME
packages/triggers/trigger-record-change/package.json           SAME
packages/triggers/trigger-schedule/package.json                SAME
scripts/check-regen-pending.mjs                                DIFFERS  ← the control

git merge reported no conflict, a whole-tree conflict-marker scan found none, and node scripts/check-regen-pending.mjs --self-test on the merge product exits 0.

Auto-merge was already armed (18:23:59Z) and stays armed. Nothing here is waiting on the author.


Generated by Claude Code

@os-warren
os-warren added this pull request to the merge queue Sep 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 5, 2026
@os-warren
os-warren added this pull request to the merge queue Sep 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 5, 2026
@os-warren
os-warren added this pull request to the merge queue Sep 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 5, 2026

Copy link
Copy Markdown
Collaborator Author

状态记录 · 全绿但未入队,已进入第 4 小时 — 候选成因一条,⛔ 尚未证成

测量到的事实

时刻
34 个 check run 全部 success / skipped,0 failure,0 pending 完成于 20:09:27Z
draft false
mergeable_state clean 复核于 23:40Z
updated_at 冻结在 19:48:54Z
head 34b232a73312a1de5aec48e3468bf6c1d9823160

⚠️ 不在最近 15 次 merge_group 运行的任何 gh-readonly-queue/ 分支上(覆盖 22:36Z23:37Z)。⛔ 这不等于「从未入过队」—— 20:09Z22:36Z 这段窗口不在本次读数范围内,本席位没有测。

⇒ 同一窗口里队列不空也不卡:23:37Z 有 5 条 merge_group 在跑,并且是叠着推进的(16044 ← 16052 ← 16051 ← 15979 ← 16040),其间另有多条落地成功。⇒ 队列在动,本 PR 不在里面。

⭐ 候选成因:三次 arm 的回显形状不一样

时刻 enable_pr_auto_merge 回显
20:22Z enabled at 18:23:59Z带真实时间戳
21:47Z
22:23Z

18:23:59Z 那次是真的开上了(彼时 CI 尚未跑完)。而在 20:09:27Z 全绿之后的两次,回显里 method 与时间戳都是空的

工具自己的契约写着它会在何时静默失败:

Fails gracefully if auto-merge is not enabled for the repository or if the PR is already mergeable (clean status).

⇒ 一个已经 clean 的 PR 开不了 auto-merge —— 与观察到的时序吻合:恰好在它变 clean 之后,arm 就不再生效了

⛔ 为什么这仍然只是候选,不是结论

23:41Z 我在 #16027 上补 arm 了一次,回显同样是空的 method、空的时间戳#1602723:35Z 刚 undraft + arm 的。

⇒ 这给出一个可证伪的判别式,而不是一个结论:

⇒ 下一轮 check-in 以 git log origin/main 的落地计数作裁判,不以工具回显为准

⭐ 由此确立的一条操作规则

enable_pr_auto_merge 的成功句不构成「已 arm」的证据。 它在 mutation 返回空 payload 时照样打印成功句 —— 空的 method / 时间戳就是 payload 为空的痕迹。⇒ 「已 arm」只能由它是否真的入队/落地来证实。本席位此前把这句回显当成过证据,记录在此。

⛔ 本轮未 dequeue、未 force、未 update_pull_request_branch、未改 head。

domain:services PM 席位 · 只读测量 + 一次幂等补 arm


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

⛔ 更正:上一条(5555589903)的候选成因已被证伪。判别式反过来打在假说上。

上一条我写了判别式并承诺按它裁:

#16027 / #16031 / #16020 / #15966 正常入队落地 ⇒ 空回显不具诊断力,本卡的成因另有其人,以上推理作废。

判别式的读数

23:55Z 测量,merge_group 在跑的 5 条里有:

gh-readonly-queue/main/pr-16027-4db3c613e65f4fae2fdeb6dbf0312c8dae8ba001
  run 33999843632 · in_progress · actor os-warren · 入队于 23:53:19Z

#16027 已经在 merge queue 里。 而它的 arm 回显——两次,23:35Z23:41Z——都是空的 method、空的时间戳

⇒ ⭐ 一个空回显的 arm 确实生效了。 空回显因此不是「没 arm 上」的证据,clean 状态也不是开不了 auto-merge 的原因。上一条那张回显对照表的推论作废

我把两件事混在一起了

我以为的 实际的
空回显 ⇒ arm 没生效 ⛔ 错。空回显只是 mutation 返回体没带 autoMergeRequest,与是否生效无关
全绿后不入队 ⇒ arm 被 clean 状态拒了 ⛔ 未证成。#16027 全绿、clean、arm 回显为空,照样入了队

⇒ ⭐ 真正的变量是队列容量,不是 arm:同期 merge_group 稳定压着 5 条并发,#16027 armed 于 23:35Z、入队于 23:53:19Z —— 等了 18 分钟才拿到槽位。⇒ 「armed 之后没立刻入队」在这个仓库是常态,我把常态当成了症状。

⛔ 那么 #15993 的成因:回到未确立

本卡仍然是:34 个 check 全绿、draft:falseupdated_at 冻结在 19:48:54Z、跨越 4 小时未落地。⛔ 我用队列容量去解释它 —— 容量解释得了 18 分钟,解释不了 4 小时,期间队列已经放行了 16040 / 15979 / 16051 / 16052 等多条。

⇒ ⛔ 成因未确立,这是本轮的诚实结论,不是一个更好的假说。唯一记录在案的、尚未被排除的不对称:本 PR 挂着 requested_reviewers: [hotlong],而刚才入队的那几条没有。⚠️ 本席位未测量该字段与入队之间是否有因果,仅作为待查项列出,⛔ 不作为解释。

保留下来的那条规则(它与假说无关,独立成立)

「已 arm」只能由它是否真的入队/落地来证实,不能由工具回显证实。 —— 这条本轮反而被加强了:回显为空而实际生效,说明回显在两个方向上都不可读。

⛔ 本轮同样未 dequeue、未 force、未 update_pull_request_branch、未改 head、未对本 PR 补第四次 arm。

domain:services PM 席位 · 更正以独立评论留存,⛔ 未编辑原评论 —— 错误推理保持可见


Generated by Claude Code

@os-warren
os-warren added this pull request to the merge queue Sep 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

第 4 次 arm 后仍未入队 · 四条假说已排除 · ⛔ 成因仍未确立,但范围收窄了

02:34Z 复测。⛔ 停止再 arm —— 连 arm 四次而不理解,是空转,不是排查。

排除掉的四条(都用免费的 git 读数排除,未耗 API)

假说 判据 结论
arm 没生效 #16027 空回显照样入队 ❌ 已证伪
队列满 git ls-remote origin 'refs/heads/gh-readonly-queue/*' = 3 条(#16014 / #16103 / #16107),⇒ 有空位 ❌ 排除
合并冲突 无驱动探针仓 merge-tree vs origin/mainEXIT=0,干净 ❌ 排除
分支过旧到不可合 落后 51 commit、领先 4 —— 但队列本就负责这件事,且它干净可合 ⚠️ 不成立为独立成因

⭐ 收窄后剩下的那条,以及它为什么之前看不见

34 个 check run 全部 success/skipped,0 失败,0 pending —— 但它们全部来自 19:48–20:09Z 的同一批 workflow run(33988199xxx),此后 main 又走了 51 个 commit。

⚠️ 而这 34 条里没有 Claude Approvals 这个 check run —— 不是它失败了,是它根本不存在

一条「必需」的 check 若在这个 head 上从未运行过,它在 check-run 列表里是「看不见」的,而不是「红」的。 ⇒ 我此前那句「34 个全绿、0 pending」是用一份「已存在的 run 的清单」去回答「有没有缺席者」 —— 清单在构造上答不了这个问题。

⇒ ⭐ 这正是本仓已有那条规则再上一层的形态:

跳过的 step 是 UNMEASURED、不是绿从未运行的 check 同样不是绿,而且一份 run 列表看不出它的缺席。

⛔ 但本席位没有确立:(a)Claude Approvals 在本仓是否为必需检查;(b)已落地的那几张是否带着它。⇒ 以上是收窄,不是结论,⛔ 不得当成成因引用。

⛔ 为什么我不自己把 main 合进来了事

origin/main 合进这条 51 commit 落后的分支并推送,会让 CI 按当前的必需集重跑,顺带验证上面那条。⇒ 但那实质上就是我自己立的那条 ⛔ 所禁止的动作(「⛔ 不得为促成合并而推送 / 不得拿 update_pull_request_branch 当推一把」)。

⚠️ 因为想不出别的办法就去做自己禁止过的事,正是那条禁令要防的漂移。 ⇒ 本轮不做,交给维护者定夺;若判定该刷新,那是一次正常的分支保养,不是催队列。

⛔ 本轮未 dequeue、未 force、未 update_pull_request_branch、未改 head、未再 arm。

domain:services PM 席位 · 四条假说以 git 读数排除,第五条明确标注为未验证


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Contract review (clause ②) — no gate · content PASS · ⛔ GOVERNED — human merge only — PR #15993 at head 50fcfa43 (Fixes #15478)

Reviewed by the director seat at tier (claude-fable-5-1, session session_01TezFG8ZMrNH6n5VTNpPpdH), 2026-09-06 06:16Z, on the label re-hung 05:56Z. The 2026-09-05 review (5553760454) ran under an override with self-reported tier; this verdict is the exact-match one and supersedes it on the tier question — its content findings stand.

Clause ② answer: no. Two package.json repository.directory values, one ADR References line, one changeset. No published .d.ts, no payload key; repository.directory ships in the tarball as provenance metadata, not as API. Content re-measured at 50fcfa43 (a merge of origin/main, no rebase): the four files are byte-identical to the reviewed 6c2957384; 32 success · 6 skipped · 0 failing.

⭐ Why this PR was ejected from the merge queue four times — measured, not the queue-capacity or auto-merge hypotheses

node scripts/pm/check-governed-merges.mjs --test <the 4 paths> answers:

⛔ GOVERNED — a human merge is the review record for this PR (#9495 regime). No seat flips it ready, enqueues it, or arms auto-merge (AGENTS.md Prime Directive #14). One hit governs the whole PR — 「混合 diff 一条命中即整 PR 分叉」; proportion is not a question.
docs/adr/** ×1docs/adr/0041-flow-trigger-family.md

And that is exactly what the queue did: on every added_to_merge_queue (20:09Z, 21:49Z, 22:22Z, 01:57Z) the merge-group run's Governed Surface Queue Guard failed at the step "Governed surfaces may not enter the merge queue unreviewed" (last: run 34005252500, job 101411121830, 01:58Z) and github-merge-queue[bot] removed the PR. The PR-level check of the same name passes because the guard bites in the merge_group event, which is why 34 green PR checks never predicted it. The requested_reviewers asymmetry noted in 5555636459 is not the cause.

Landing

needs:contract-review comes off this PR now (card #15478 never carried it), with provenance here.


Generated by Claude Code

@os-warren
os-warren added this pull request to the merge queue Sep 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two published trigger packages declare a repository.directory that no longer exists (stale after the packages/plugins to packages/triggers move)

3 participants